/*
 * db_mysql.c
 *
 * Copyright (C) 2011 - Dr.NP
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @version db_mysql.c 1.0
 * @package storage
 * @author Dr.NP <np@bsgroup.org>
 * @update 08/17/2011
 */

/**
 * MySQL database operator
 * 
 * === CHANGELOG ===
 * [08/17/2011] - Creation
 */

#include "bsp.h"

#include "bsp_conf.h"
#include "bsp_hash.h"
#include "bsp_object.h"
#include "bsp_utils.h"
#include "bsp_db_mysql.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <err.h>
#include <sys/types.h>

#include "mysql.h"
#include "errmsg.h"

struct mysql_conn *mysql_conn_list;
size_t mysql_conn_list_size;

int db_mysql_init()
{
    mysql_conn_list = ensure_list_space(NULL, sizeof(struct mysql_conn), &mysql_conn_list_size, MYSQL_CONN_LIST_INITIAL);
    if (!mysql_conn_list)
    {
        err(RTN_ERROR_MEMORY_ALLOC, "MySQL connection list initial error!!!");
    }

    memset(mysql_conn_list, 0, sizeof(struct mysql_conn) * mysql_conn_list_size);

    debug_info("Storage :: MySQL initialized\n");

    return RTN_SUCCESS;
}

int db_mysql_connect(
                                     const char *host, 
                                     const char *user, 
                                     const char *pass, 
                                     const char *db, 
                                     int max_active_results
                                     )
{
    // Caculate the hash value
    char srv_sig[4096];
    snprintf(srv_sig, 
             4095, 
             "%s:%s@%s/%s#%d", 
             user ? user : "", 
             pass ? pass : "", 
             host ? host : "", 
             db ? db : "", 
             max_active_results);
    u_int32_t srv_hash = hash(srv_sig, strlen(srv_sig));

    // Find available connection
    struct mysql_conn *curr = NULL;
    int i;
    for (i = 0; i < mysql_conn_list_size; i ++)
    {
        if ((!mysql_conn_list[i].mask & CONN_MASK_IN_USE) && mysql_conn_list[i].srv_hash == srv_hash)
        {
            // Available connection
            curr = &mysql_conn_list[i];
            curr->mask |= CONN_MASK_IN_USE;

            return curr->id;
        }
    }

    // Make a new one?
    for (i = 0; i < mysql_conn_list_size; i ++)
    {
        if (!mysql_conn_list[i].srv_hash && !mysql_conn_list[i].mask)
        {
            // An empty slot
            curr = &mysql_conn_list[i];
            curr->id = i;

            break;
        }
            
    }

    if (!curr)
    {
        // Enlarge conn list
        mysql_conn_list = ensure_list_space(mysql_conn_list, sizeof(struct mysql_conn), &mysql_conn_list_size, mysql_conn_list_size * 2);
        if (!mysql_conn_list)
        {
            err(RTN_ERROR_MEMORY_ALLOC, "MySQL connection list initial error!!!");
        }

        i = (mysql_conn_list_size / 2);
        memset(mysql_conn_list + i * sizeof(struct mysql_conn), 0, sizeof(struct mysql_conn));
        curr = &mysql_conn_list[i];
        curr->id = i;
    }
    
    const char *conn_host = (const char *) NULL;
    const char *conn_sock = (const char *) NULL;
    const char *conn_user = user ? user : "";
    // For the password, a value of the empty string in the mysql_real_connect() 
    // call cannot be overridden in an option file, because the empty string indicates 
    // explicitly that the MySQL account must have an empty password.
    const char *conn_pass = pass ? pass : (const char *) NULL;
    const char *conn_db = db ? db : (const char *) NULL;
    
    // If "host" is a local sock pipe?
    FILE *tfp = fopen(host, "r");
    if (tfp)
    {
        // Local sock file
        conn_sock = host;
        fclose(tfp);
    }

    else
    {
        conn_host = host;
    }

    curr->conn = mysql_init(NULL);
    if (!mysql_real_connect(curr->conn, 
                            conn_host, 
                            conn_user, 
                            conn_pass, 
                            conn_db, 
                            0, 
                            conn_sock, 
                            0))
    {
        return RTN_ERROR_DB_CONNECTION;
    }

    my_bool my_true = 0x1;
    mysql_options(curr->conn, MYSQL_OPT_RECONNECT, &my_true);

    // Set default charset
    mysql_query(curr->conn, "SET NAMES 'utf8'");

    // Set default database
    if (conn_db)
    {
        mysql_select_db(curr->conn, conn_db);
    }

    // Set hash
    curr->srv_hash = srv_hash;

    // Set mask
    curr->mask |= CONN_MASK_IN_USE;

    if (db && strlen(db))
    {
        curr->mask |= CONN_MASK_DB_SET;
    }

    curr->result_list_size = max_active_results;
    if (curr->result_list)
    {
        db_mysql_free_results(curr->id);
        
        free(curr->result_list);
    }

    curr->result_list = ensure_list_space(NULL, sizeof(MYSQL_RES *), NULL, max_active_results);
    memset(curr->result_list, 0, sizeof(MYSQL_RES *) * max_active_results);

    return curr->id;
}

int db_mysql_close(int conn_id)
{
    struct mysql_conn *curr = NULL;

    if (conn_id >= 0 && conn_id < mysql_conn_list_size)
    {
        curr = &mysql_conn_list[conn_id];
    }

    if (curr && curr->conn)
    {
        db_mysql_free_results(curr->id);
        mysql_close(curr->conn);
        curr->mask &= ~CONN_MASK_IN_USE;
    }

    return RTN_SUCCESS;
}

int db_mysql_query(int conn_id, const char *query, size_t stmt_len)
{
    struct mysql_conn *curr = NULL;
    int res, i;

    if (conn_id >= 0 && conn_id < mysql_conn_list_size)
    {
        curr = &mysql_conn_list[conn_id];
    }
    
    if (curr && curr->conn && curr->mask & CONN_MASK_IN_USE)
    {
        if ((signed) stmt_len < 0)
        {
            stmt_len = strlen(query);
        }
        res = mysql_real_query(curr->conn, query, stmt_len);

        if (0 == res)
        {
            // Query succssfully
            curr->queries ++;

            MYSQL_RES *qres = mysql_store_result(curr->conn);
            int res_id = -1;
            if (qres)
            {
                // SELECT / SHOW / DESCRIBE / EXPLAIN / CHECK TABLE ...
                // A result set returned, and stored into object
                // Find a empty result slot
                for (i = 0; i < curr->result_list_size; i ++)
                {
                    if (!curr->result_list[i])
                    {
                        curr->result_list[i] = qres;
                        res_id = i;

                        return res_id;
                    }
                }

                if (res_id < 0)
                {
                    // Result list full, you have to free some used result first!!!
                    return RTN_ERROR_DB_RESULT_FULL;
                }
            }

            else
            {
                // Statement didn't return a result set
            }
            
            return RTN_SUCCESS;
        }

        else if (CR_SERVER_GONE_ERROR == res || CR_SERVER_LOST == res)
        {
            // MySQL gone
            db_mysql_close(curr->id);

            return RTN_ERROR_DB_CONNECTION;
        }

        else
        {
            return RTN_ERROR_DB_UNKNOWN;
        }
    }

    else
    {
        res = RTN_ERROR_DB_UNAVAILABLE; 
    }

    return res;
}

const char * db_mysql_error(int conn_id)
{
    struct mysql_conn *curr = NULL;
    
    if (conn_id >= 0 && conn_id < mysql_conn_list_size)
    {
        curr = &mysql_conn_list[conn_id];
    }

    if (curr && curr->conn && curr->mask & CONN_MASK_IN_USE)
    {
        return mysql_error(curr->conn);
    }

    return "";
}

int db_mysql_free_result(int conn_id, int res_id)
{
    struct mysql_conn *curr = NULL;
    
    if (conn_id >= 0 && conn_id < mysql_conn_list_size)
    {
        curr = &mysql_conn_list[conn_id];
    }

    else
    {
        return RTN_ERROR_DB_UNAVAILABLE;
    }

    if (curr && curr->conn && curr->result_list && res_id >= 0 && res_id < curr->result_list_size)
    {
        if (curr->result_list[res_id])
        {
            mysql_free_result(curr->result_list[res_id]);

            curr->result_list[res_id] = NULL;
        }
    }

    else
    {
        return RTN_ERROR_DB_INVALID_RESULT;
    }

    return RTN_SUCCESS;
}

int db_mysql_free_results(int conn_id)
{
    struct mysql_conn *curr = NULL;
    
    if (conn_id >= 0 && conn_id < mysql_conn_list_size)
    {
        curr = &mysql_conn_list[conn_id];
    }

    else
    {
        return RTN_ERROR_DB_UNAVAILABLE;
    }

    if (curr && curr->conn && curr->result_list)
    {
        int i;

        for (i = 0; i < curr->result_list_size; i ++)
        {
            if (curr->result_list[i])
            {
                mysql_free_result(curr->result_list[i]);

                curr->result_list[i] = NULL;
            }
        }
    }

    return RTN_SUCCESS;
}

u_int64_t db_mysql_num_rows(int conn_id, int res_id)
{
    struct mysql_conn *curr = NULL;
    u_int64_t rows = 0;
    
    if (conn_id >= 0 && conn_id < mysql_conn_list_size)
    {
        curr = &mysql_conn_list[conn_id];
    }

    else
    {
        return RTN_ERROR_DB_UNAVAILABLE;
    }

    if (curr && curr->conn && curr->result_list && res_id >= 0 && res_id < curr->result_list_size)
    {
        if (curr->result_list[res_id])
        {
            rows = mysql_num_rows(curr->result_list[res_id]);
        }
    }

    return rows;
}

int db_mysql_fetch_row(int conn_id, int res_id, struct object_t *obj)
{
    struct mysql_conn *curr = NULL;
    struct kv_t *item;
    MYSQL_ROW row;
    MYSQL_FIELD *fields;
    unsigned long *lengths;
    u_int32_t num_fields;
    int i;

    if (!obj)
    {
        return RTN_ERROR_MEMORY_OBJECT;
    }

    if (conn_id >= 0 && conn_id < mysql_conn_list_size)
    {
        curr = &mysql_conn_list[conn_id];
    }

    else
    {
        return RTN_ERROR_DB_UNAVAILABLE;
    }

    if (curr && curr->conn && curr->result_list && res_id >= 0 && res_id < curr->result_list_size)
    {
        if (curr->result_list[res_id])
        {
            row = mysql_fetch_row(curr->result_list[res_id]);
            fields = mysql_fetch_fields(curr->result_list[res_id]);
            lengths = mysql_fetch_lengths(curr->result_list[res_id]);
            num_fields = mysql_num_fields(curr->result_list[res_id]);

            if (row && fields && lengths)
            {
                for (i = 0; i < num_fields; i ++)
                {
                    // Store result
                    item = alloc_item((const char *) fields[i].name, -1);
                    set_string_item(item, (const char *) row[i], lengths[i]);
                    insert_item(obj, item);
                }

                return num_fields;
            }
        }
    }

    return RTN_SUCCESS;
}
